package pl.edu.mimuw.mas.visualization.rendering;

import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.glu.GLU;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.edu.mimuw.mas.agent.AgentPossition;
import pl.edu.mimuw.mas.bezier.ControlPoints;
import pl.edu.mimuw.mas.visualization.agent.AgentPossitionManager;

import com.sun.opengl.util.GLUT;

/**
 * Silnik renderujący sceny dla obiektu {@link Chart} dla parametrów zadanych
 * kontekstem {@link ChartRendererContext}.
 */
public class ChartRenderer implements IRenderer {

	private final Logger log = LoggerFactory.getLogger(getClass());
	
	private static final int X = 0, Y = 1, Z = 2;
	private static final int R = 0, G = 1, B = 2, A = 3;
	private static final int X0 = 0, Y0 = 1, X1 = 2, Y1 = 3;

	private final GLU glu;
	private final GLUT glut;
			
	private int glCube;	
	private int prevMouseX, prevMouseY;
	private boolean ignoreMouseEvent;
	
	private ChartRendererContext context;
	private AgentPossitionManager possitionManager;

	public ChartRenderer(ChartRendererContext context) {
		this.glu = new GLU();
		this.glut = new GLUT();
		setContext(context);
	}

	public void setContext(ChartRendererContext context) {
		if (context == null) {
			log.debug("Niemożna zmienić kontekstu na pusty");
			throw new NullPointerException();			
		}

		this.context = context;
	}
	
	public void setPossitionManager(AgentPossitionManager possitionManager) {
		if (possitionManager == null) {
			log.debug("Niemożna zmienić managera pozycji agentów na pusty");
			throw new NullPointerException();			
		}

		this.possitionManager = possitionManager;
	}

	@Override
	public void init(GLAutoDrawable drawable) {
		GL gl = drawable.getGL();
		
		log.info("OpenGL : " + gl.getClass().getName());
		log.info(drawable.getChosenGLCapabilities().toString());

		// ustawia parametry światła
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, context.getLightPosition(), 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, context.getLightAmbient(), 0);
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glEnable(GL.GL_LIGHTING);
		gl.glEnable(GL.GL_LIGHT0);
		gl.glEnable(GL.GL_NORMALIZE);

		// tło
		float[] bg = context.getBgColor();
		gl.glClearColor(bg[R], bg[G], bg[B], bg[A]);
		gl.glClearDepth(1.0f);

		gl.glDepthFunc(GL.GL_LESS);
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glShadeModel(GL.GL_SMOOTH);

		// gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);

		// rysowanie punktów kontrolnych - rejestracja w cache OpenGL
		glCube = gl.glGenLists(1);
		if (glCube != 0) {
			gl.glNewList(glCube, GL.GL_COMPILE);
			drawCube(gl);
			gl.glEndList();
			log.info("Punkt kontrolny dodany do cache OpenGL");
		} else {
			log.error("Punkt kontrolny niemoże być dodany do cache OpenGL");
			throw new RuntimeException();
		}
		
		// rejestracja zdarzeń
		drawable.addMouseListener(this);
		drawable.addMouseMotionListener(this);
		drawable.addKeyListener(this);

		log.info("Silnik renderujący zainicjalizowany");
	}

	@Override
	public void display(GLAutoDrawable drawable) {
		GL gl = drawable.getGL();

		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glPushMatrix();

		float[] pos = context.getChartPosition();
		float[] rot = context.getViewAngle();
		float[] cen = context.getCenter();
		float scale = context.getScale();

		// pomniejsz i przesuń
		gl.glTranslatef(pos[X], pos[Y], pos[Z]);
		gl.glScalef(scale, scale, scale);
		// obróć
		gl.glRotatef(rot[Y], 0.0f, 1.0f, 0.0f);
		gl.glRotatef(rot[X], (float) Math.cos(Math.toRadians(rot[Y])), 0.0f, (float) Math.sin(Math.toRadians(rot[Y])));
		// wyśrodkuj
		gl.glTranslatef(-cen[X], -cen[Y], cen[Z]);

		drawGrid(gl);
		switch (context.getType()) {
		case EDITOR:
			drawControlPoints(gl);
			break;
		case VISUALIZATION:
			drawAgents(gl);
			break;
		}
		
		gl.glPopMatrix();
		gl.glFlush();
	}

	@Override
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL gl = drawable.getGL();

		log.info("GL_VENDOR: " + gl.glGetString(GL.GL_VENDOR));
		log.info("GL_RENDERER: " + gl.glGetString(GL.GL_RENDERER));
		log.info("GL_VERSION: " + gl.glGetString(GL.GL_VERSION));

		// rzutowanie
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(context.getFrustrumAngle(), (float) width / (float) height, context.getFrustrumNear(), context.getFrustrumFar());

		// model danych
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();			
	}

	/**
	 * Rysuje siatkę mapy.
	 */
	private void drawGrid(GL gl) {
		float dst = context.chart.getDistance();
		int mrg = context.getViewMargin();

		boolean show = context.isSelected();
		int[] sel = context.getSelection();

		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);

		// linie pionowe
		for (int c = Math.max(context.viewArea[X0] - mrg, 0); c < Math.min(context.viewArea[X1] + mrg + 1, context.chart.getColumnsCount()); c++) {
			gl.glBegin(GL.GL_LINE_STRIP);
			for (int r = Math.max(context.viewArea[Y0] - mrg, 0); r < Math.min(context.viewArea[Y1] + mrg + 1, context.chart.getRowsCount()); r++) {
				if (show && c >= sel[X0] && c <= sel[X1]) {
					if (r == sel[Y0]) {
						gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE,	context.getActiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
					if (r == sel[Y1]) {
						gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE,	context.getInactiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
				}
				gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
			}
			gl.glEnd();
		}

		// linie poziome
		for (int r = Math.max(context.viewArea[Y0] - mrg, 0); r < Math.min(context.viewArea[Y1] + mrg + 1, context.chart.getRowsCount()); r++) {
			gl.glBegin(GL.GL_LINE_STRIP);
			for (int c = Math.max(context.viewArea[X0] - mrg, 0); c < Math.min(context.viewArea[X1] + mrg + 1, context.chart.getColumnsCount()); c++) {
				if (show && r >= sel[Y0] && r <= sel[Y1]) {
					if (c == sel[X0]) {
						gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getActiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
					if (c == sel[X1]) {
						gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
				}
				gl.glVertex3f(c * dst, context.chart.getHeight(c, r), -r * dst);
			}
			gl.glEnd();
		}

		// linie ukośne 
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);
		for (int r = context.viewArea[Y0] + 1; r <= context.viewArea[Y1]; r++) {
			gl.glBegin(GL.GL_LINE_STRIP);
			for (int c = 0; c <= Math.min(r - context.viewArea[Y0], context.viewArea[X1] - context.viewArea[X0]); c++) {

				int cc = context.viewArea[X0] + c;
				int rr = r - c;

				if (show) {
					if ((cc == sel[X0] && rr > sel[Y0] && rr <= sel[Y1]) || (rr == sel[Y1] && cc > sel[X0] && cc < sel[X1])) {
						gl.glVertex3f(cc * dst, context.chart.getHeight(cc, rr), -rr * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getActiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
					if (cc == sel[X1] || rr == sel[Y0]) {
						gl.glVertex3f(cc * dst, context.chart.getHeight(cc, rr), -rr * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
				}
				gl.glVertex3f(cc * dst, context.chart.getHeight(cc, rr), -rr
						* dst);
			}
			gl.glEnd();
		}

		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);
		for (int c = context.viewArea[X0] + 1; c <= context.viewArea[X1]; c++) {
			gl.glBegin(GL.GL_LINE_STRIP);
			for (int r = 0; r <= Math.min(context.viewArea[X1] - c, context.viewArea[Y1] - context.viewArea[Y0]); r++) {

				int cc = c + r;
				int rr = context.viewArea[Y1] - r;

				if (show) {
					if ((cc == sel[X0] && rr > sel[Y0] && rr <= sel[Y1]) || (rr == sel[Y1] && cc > sel[X0] && cc < sel[X1])) {
						gl.glVertex3f(cc * dst,	context.chart.getHeight(cc, rr), -rr * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE,	context.getActiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
					if (cc == sel[X1] || rr == sel[Y0]) {
						gl.glVertex3f(cc * dst, context.chart.getHeight(cc, rr), -rr * dst);
						gl.glEnd();

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveGridColor(), 0);
						gl.glBegin(GL.GL_LINE_STRIP);
					}
				}
				gl.glVertex3f(cc * dst, context.chart.getHeight(cc, rr), -rr * dst);
			}
			gl.glEnd();
		}
	}

	/**
	 * Rysuje siatkę kontrolną.
	 */
	private void drawControlPoints(GL gl) {
		ControlPoints cp = context.getControlPoints();

		if (cp != null) {
			int[] area = cp.getArea().getArea();

			// odległość między liniami siatki kontrolnej
			float dst = context.chart.getDistance();
			float xdst = (area[X1] - area[X0]) * dst / (cp.getColumnsCount() - 1);
			float ydst = (area[Y1] - area[Y0]) * dst / (cp.getRowsCount() - 1);

			gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getInactiveControlPointColor(), 0);

			// siatka kontorolna
			for (int c = 0; c < cp.getColumnsCount(); c++) {
				gl.glBegin(GL.GL_LINE_STRIP);
				for (int r = 0; r < cp.getRowsCount(); r++) {
					gl.glVertex3f(area[X0] * dst + c * xdst, cp.getHeight(c, r), -area[Y0] * dst - r * ydst);
				}
				gl.glEnd();
			}
			for (int r = 0; r < cp.getRowsCount(); r++) {
				gl.glBegin(GL.GL_LINE_STRIP);
				for (int c = 0; c < cp.getColumnsCount(); c++) {
					gl.glVertex3f(area[X0] * dst + c * xdst, cp.getHeight(c, r), -area[Y0] * dst - r * ydst);
				}
				gl.glEnd();
			}

			// rozmiar punktów kontrolnych
			float scale = context.getNodeScale();
			
			for (int c = 0; c < cp.getColumnsCount(); c++) {
				for (int r = 0; r < cp.getRowsCount(); r++) {
					if (cp.isSelected(c, r)) {
						gl.glPushMatrix();
						gl.glTranslatef(area[X0] * dst + c * xdst, cp.getHeight(c, r), -area[Y0] * dst - r * ydst);
						gl.glScalef(scale, scale, scale);

						gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, context.getActiveControlPointColor(), 0);
						gl.glCallList(glCube);

						gl.glPopMatrix();
					}
				}
			}
		}
	}
	
	private void drawAgents(GL gl) {
		float scale = context.getNodeScale();
		float dst = context.chart.getDistance();		
		
		for (AgentPossition pos : possitionManager.getAllAgentPossitions()) {
			gl.glPushMatrix();
			gl.glTranslatef(pos.getX() * dst, pos.getZ(), - pos.getY() * dst);
			gl.glScalef(scale, scale, scale);

			gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, pos.getColor(), 0);
			gl.glCallList(glCube);

			gl.glRasterPos3f(0, dst, dst);
			glut.glutBitmapString(GLUT.BITMAP_HELVETICA_12, pos.getLabel());

			gl.glPopMatrix();
		}
	}

	/**
	 * Rysuje kostke o boku długości 2 o środku w (0, 0, 0).
	 */
	private void drawCube(GL gl) {
		gl.glBegin(GL.GL_QUADS);
		gl.glNormal3f(0.0f, -1.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		
		gl.glNormal3f(0.0f, 1.0f, 0.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		
		gl.glNormal3f(0.0f, 0.0f, 1.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		
		gl.glNormal3f(0.0f, 0.0f, -1.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		
		gl.glNormal3f(1.0f, 0.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		
		gl.glNormal3f(-1.0f, 0.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		gl.glEnd();
	}

	@Override
	public void mousePressed(MouseEvent e) {
		if (e.getButton() == MouseEvent.BUTTON1) {
			prevMouseX = e.getX();
			prevMouseY = e.getY();
			ignoreMouseEvent = false;
		} else {
			ignoreMouseEvent = true;
		}
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		if (! ignoreMouseEvent) {
			int x = e.getX();
			int y = e.getY();
			Dimension size = e.getComponent().getSize();

			float thetaY = 180.0f * ((float) (x - prevMouseX) / (float) size.width);
			float thetaX = 180.0f * ((float) (prevMouseY - y) / (float) size.height);

			prevMouseX = x;
			prevMouseY = y;
			
			context.incViewAngle(Y, thetaY);						
			context.incViewAngle(X, -thetaX);
		}
	}

	@Override
	public void keyPressed(KeyEvent e) {
		// log.debug("Naciśnięto przycisk: " + e.getKeyCode());
		
		switch (e.getKeyCode()) {
		case 37: // <-
			context.moveChart(X, -5f);
			break;
		case 39: // ->
			context.moveChart(X, 5f);
			break;
		case 33: // PageUp
			context.moveChart(Z, -10f);
			break;
		case 34: // PageDown
			context.moveChart(Z, 10f);
			break;
		case 38: // góra
			context.moveChart(Y, 5f);
			break;
		case 40: // dół
			context.moveChart(Y, -5f);
			break;
		case 27: // ESC
			context.restoreDefaultView();
			break;
		case 61: // = (+)
			context.incScale();
			break;
		case 45: // -
			context.decScale();
			break;
		}
	}

	@Override
	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
	}

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}

	@Override
	public void mouseMoved(MouseEvent e) {
	}

	@Override
	public void mouseReleased(MouseEvent e) {
	}

	@Override
	public void keyReleased(KeyEvent e) {
	}

	@Override
	public void keyTyped(KeyEvent e) {
	}
}
